﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.XPath;
using System.Xml;
using System.IO;

namespace XML2Stars
{
	enum XMLElement
	{
		StarList,
		Star,
		ID,
		Code,
		Name,
	}

	enum XMLAttribute
	{
		RightAscension,
		Declination,
		Magnitude,
		Catalog,
		Color,
		BV,
		Type,
	}

	enum StarCatalog
	{
		BSC,
		HIP,
		Other
	}

	internal static class ReaderExtensions
	{
		const string NS = "urn:denebsoft-skymap-star";

		internal static XPathNodeIterator SelectChildren(this XPathNavigator reader, XMLElement name)
		{
			return reader.SelectChildren(name.ToString(), NS);
		}

		internal static XPathNodeIterator SelectChildren(this XPathNavigator reader, XMLElement name, string xpath, params object[] args)
		{
			var nsman = new XmlNamespaceManager(reader.NameTable);
			nsman.AddNamespace("tmp", NS);
			if (args.Length > 0)
			{
				var xargs = new object[args.Length];
				for (int k = 0; k < args.Length; k++)
					if (args[k] is XMLElement)
						xargs[k] = "tmp:" + args[k].ToString();
					else if (args[k] is XMLAttribute)
						xargs[k] = "@" + args[k].ToString();
					else
						xargs[k] = args[k];
				xpath = String.Format(xpath, xargs);
			}
			return reader.Select(String.Format("tmp:{0}[{1}]", name, xpath), nsman);
		}

		internal static void ForEachChildren(this XPathNavigator reader, XMLElement name, Action<XPathNavigator> body)
		{
			var childs = reader.SelectChildren(name.ToString(), NS);
			foreach (XPathNavigator child in childs)
				body(child);
		}

		internal static void ForEachChildren(this XPathNavigator reader, XMLElement name, Action<int> count, Action<XPathNavigator, int> body)
		{
			var childs = reader.SelectChildren(name.ToString(), NS);
			count(childs.Count);
			int index = 0;
			foreach (XPathNavigator child in childs)
				body(child, index++);
		}

		internal static float GetAttributeSingle(this XPathNavigator reader, XMLAttribute name)
		{
			string value = reader.GetAttribute(name.ToString(), String.Empty);
			if (String.IsNullOrEmpty(value))
				throw new ArgumentNullException(name.ToString());
			return XmlConvert.ToSingle(value);
		}

		internal static double? GetAttributeDouble(this XPathNavigator reader, XMLAttribute name)
		{
			string value = reader.GetAttribute(name.ToString(), String.Empty);
			if (String.IsNullOrEmpty(value))
				return null;
			return XmlConvert.ToDouble(value);
		}

		internal static TEnum GetAttribute<TEnum>(this XPathNavigator reader, XMLAttribute name, TEnum defValue) where TEnum: struct
		{
			string value = reader.GetAttribute(name.ToString(), String.Empty);
			if (String.IsNullOrEmpty(value))
				return defValue;
			return (TEnum)Enum.Parse(typeof(TEnum), value);
		}

		internal static string GetAttribute(this XPathNavigator reader, XMLAttribute name)
		{
			return reader.GetAttribute(name.ToString(), String.Empty);
		}

		internal static string GetAttributeLanguage(this XPathNavigator reader)
		{
			return reader.XmlLang;
		}

		internal static int GetInt32(this XPathNavigator reader)
		{
			return XmlConvert.ToInt32(reader.Value);
		}
	}

	internal static class WriterExtensions
	{
		static double[] StarBV = new double[]
		{
			//#FFFFFF
			-0.40, //#9BB2FF; B-V = -0.40; Teff = 113017
			-0.35, //#9EB5FF; B-V = -0.35; Teff = 56701
			-0.30, //#A3B9FF; B-V = -0.30; Teff = 33605
			-0.25, //#AABFFF; B-V = -0.25; Teff = 22695
			-0.20, //#B2C5FF; B-V = -0.20; Teff = 16954
			-0.15, //#BBCCFF; B-V = -0.15; Teff = 13674
			-0.10, //#C4D2FF; B-V = -0.10; Teff = 11677
			-0.05, //#CCD8FF; B-V = -0.05; Teff = 10395
			0.00, //#D3DDFF; B-V = -0.00; Teff = 9531
			0.05, //#DAE2FF; B-V = 0.05; Teff = 8917
			0.10, //#DFE5FF; B-V = 0.10; Teff = 8455
			0.15, //#E4E9FF; B-V = 0.15; Teff = 8084
			0.20, //#E9ECFF; B-V = 0.20; Teff = 7767
			0.25, //#EEEFFF; B-V = 0.25; Teff = 7483
			0.30, //#F3F2FF; B-V = 0.30; Teff = 7218
			0.35, //#F8F6FF; B-V = 0.35; Teff = 6967
			0.40, //#FEF9FF; B-V = 0.40; Teff = 6728
			0.45, //#FFF9FB; B-V = 0.45; Teff = 6500
			0.50, //#FFF7F5; B-V = 0.50; Teff = 6285
			0.55, //#FFF5EF; B-V = 0.55; Teff = 6082
			0.60, //#FFF3EA; B-V = 0.60; Teff = 5895
			0.65, //#FFF1E5; B-V = 0.65; Teff = 5722
			0.70, //#FFEFE0; B-V = 0.70; Teff = 5563
			0.75, //#FFEDDB; B-V = 0.75; Teff = 5418
			0.80, //#FFEBD6; B-V = 0.80; Teff = 5286
			0.85, //#FFE9D2; B-V = 0.85; Teff = 5164
			0.90, //#FFE8CE; B-V = 0.90; Teff = 5052
			0.95, //#FFE6CA; B-V = 0.95; Teff = 4948
			1.00, //#FFE5C6; B-V = 1.00; Teff = 4849
			1.05, //#FFE3C3; B-V = 1.05; Teff = 4755
			1.10, //#FFE2BF; B-V = 1.10; Teff = 4664
			1.15, //#FFE0BB; B-V = 1.15; Teff = 4576
			1.20, //#FFDFB8; B-V = 1.20; Teff = 4489
			1.25, //#FFDDB4; B-V = 1.25; Teff = 4405
			1.30, //#FFDBB0; B-V = 1.30; Teff = 4322
			1.35, //#FFDAAD; B-V = 1.35; Teff = 4241
			1.40, //#FFD8A9; B-V = 1.40; Teff = 4159
			1.45, //#FFD6A5; B-V = 1.45; Teff = 4076
			1.50, //#FFD5A1; B-V = 1.50; Teff = 3989
			1.55, //#FFD29C; B-V = 1.55; Teff = 3892
			1.60, //#FFD096; B-V = 1.60; Teff = 3779
			1.65, //#FFCC8F; B-V = 1.65; Teff = 3640
			1.70, //#FFC885; B-V = 1.70; Teff = 3463
			1.75, //#FFC178; B-V = 1.75; Teff = 3234
			1.80, //#FFB765; B-V = 1.80; Teff = 2942
			1.85, //#FFA94B; B-V = 1.85; Teff = 2579
			1.90, //#FF9523; B-V = 1.90; Teff = 2150
			1.95, //#FF7B00; B-V = 1.95; Teff = 1675
			2.00, //#FF5200; B-V = 2.00; Teff = 1195
		};

		internal static void Write(this BinaryWriter writer, double? value)
		{
			if (!value.HasValue)
				throw new ArgumentNullException("value");
			writer.Write(value.Value);
		}

		internal static void WriteBV(this BinaryWriter writer, double? BV)
		{
			int index = 0;
			if (BV.HasValue)
			{
				index = Array.IndexOf(StarBV, BV.Value);
				if (index < 0)
					throw new IndexOutOfRangeException("BV");
				index++;
			}
			writer.Write((byte)index);
		}

		internal static void WriteFlagString(this BinaryWriter writer, string value)
		{
			bool empty = value == null;
			writer.Write(!empty);
			if (!empty)
				writer.Write(value);
		}
	}

	class Program
	{
		static void Main(string[] args)
		{
			string ifn = args.Length != 1 && args.Length != 3 ? "stars.xml" : args[0];
			string ofn1 = args.Length == 2 ? args[0] : args.Length == 3 ? args[1] : "stars.dat";
			string ofn2 = args.Length == 2 ? args[1] : args.Length == 3 ? args[2] : "starnames.dat";

			if (args.Length > 3 || Path.GetExtension(ifn) != ".xml" || Path.GetExtension(ofn1) != ".dat" || Path.GetExtension(ofn2) != ".dat")
			{
				Console.WriteLine("usage: {0} [stars.xml] [stars.dat starnames.dat]", Path.GetFileNameWithoutExtension(System.Reflection.Assembly.GetExecutingAssembly().Location));
				return;
			}

			var doc = new XPathDocument(ifn);
			var reader = doc.CreateNavigator();
			using (var ofs1 = new FileStream(ofn1, FileMode.Create))
			using (var writer1 = new BinaryWriter(ofs1))
			using (var ofs2 = new FileStream(ofn2, FileMode.Create))
			using (var writer2 = new BinaryWriter(ofs2))
			{
				reader.MoveToFirstChild(); //StarList

				int count2 = 0;
				reader.ForEachChildren(XMLElement.Star, count1 =>
				{
					writer1.Write(count1);
					writer2.Write(0);
				}, (star, i1) =>
				{
					var RA = star.GetAttributeSingle(XMLAttribute.RightAscension); //Right Ascension
					var Dec = star.GetAttributeSingle(XMLAttribute.Declination);
					var magnitude = star.GetAttributeSingle(XMLAttribute.Magnitude);
					var BV = star.GetAttributeDouble(XMLAttribute.BV); //B-V index

					var ids = star.SelectChildren(XMLElement.ID);
					if (!ids.MoveNext())
						throw new ArgumentNullException(XMLElement.ID.ToString());
					var Catalog = ids.Current.GetAttribute(XMLAttribute.Type, StarCatalog.Other);
					var ID = ids.Current.GetInt32();

					string BayerFlamsted = null;
					star.ForEachChildren(XMLElement.Code, code =>
					{
						if (code.GetAttribute(XMLAttribute.Type, StarCatalog.Other) == StarCatalog.BSC)
							BayerFlamsted = code.Value;
					});

					string Name = null;
					string NameRus = null;
					star.ForEachChildren(XMLElement.Name, name =>
					{
						if (Name == null)
							Name = name.Value;
						if (NameRus == null && name.GetAttributeLanguage() == "ru")
							NameRus = name.Value;
					});

					writer1.Write(magnitude);
					writer1.Write(RA);
					writer1.Write(Dec);
					writer1.Write(ID);
					writer1.Write((byte)Catalog);
					writer1.WriteBV(BV);

					if (BayerFlamsted != null || Name != null)
					{
						writer2.Write(i1);
						writer2.WriteFlagString(NameRus ?? Name);
						writer2.WriteFlagString(BayerFlamsted);
						count2++;
					}
				});
				if (count2 > 0)
				{
					writer2.Seek(0, SeekOrigin.Begin);
					writer2.Write(count2);
					writer2.Seek(0, SeekOrigin.End);
				}

				writer2.Flush();
				count2 = 0;
				long pos2 = writer2.BaseStream.Position;
				reader.ForEachChildren(XMLElement.Star, count1 =>
				{
					writer2.Write(0);
				}, (star, i1) =>
				{
					var Name = new List<string>();
					bool NameRus = false;
					star.ForEachChildren(XMLElement.Name, name =>
					{
						string lang = name.GetAttributeLanguage();
						if (!NameRus && lang == "ru")
							NameRus = true;
						else if (lang == "en" || lang == "ru" || lang == "la")
							Name.Add(name.Value);
					});
					if (!NameRus && Name.Count > 0)
						Name.RemoveAt(0);

					if (Name.Count > 0)
					{
						writer2.Write(i1);
						writer2.Write(String.Join(", ", Name));
						count2++;
					}
				});
				writer2.Seek((int)pos2, SeekOrigin.Begin);
				writer2.Write(count2);
			}
		}
	}
}
